/*	
 * Copyright (C) 2004-2005 Jonathan Bindel
 * Copyright (C) 2006-2007 Eskil Bylund 
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Timers;

using ICSharpCode.SharpZipLib.Zip.Compression;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;

using DCSharp.Backend.Managers;
using DCSharp.Backend.Objects;
using DCSharp.Backend.Protocols;
using DCSharp.Logging;

using Nmdc = DCSharp.Backend.Protocols.Nmdc;

namespace DCSharp.Backend.Connections
{
	public enum ConnectionProtocol
	{
		Unknown,
		Nmdc
	}

	public enum TransferDirection
	{
		Unknown,
		Up,
		Down
	}

	public enum TransferError
	{
		Unknown,
		Aborted,
		NoSlots,
		NotAvailable
	}

	public class TransferErrorEventArgs : EventArgs
	{
		private TransferError error;
		private string message;

		public TransferErrorEventArgs(TransferError error, string message)
		{
			this.error = error;
			this.message = message;
		}

		public TransferError Error
		{
			get { return error; }
		}

		public string Message
		{
			get { return message; }
		}
	}

	public class UploadRequestEventArgs : RequestEventArgs
	{
		private Stream response;

		public UploadRequestEventArgs(Request request) : base(request)
		{
		}

		public Stream Response
		{
			get { return response; }
			set { response = value; }
		}
	}

	public delegate void DataHandler(byte[] data, int offset, int count,
		long remaining, object context);

	public class NoSlotsException : Exception
	{
	}

	/// <summary>
	/// A direct connection to a <see cref="User"/>.
	/// </summary>
	public class UserConnection : Connection
	{
		public event EventHandler<UploadRequestEventArgs> UploadRequest;

		public event EventHandler TransferStarted;
		public event EventHandler TransferCompleted;
		public event EventHandler<TransferErrorEventArgs> TransferAborted;

		private static Logger log = LogManager.GetLogger("UserConnection");
		private object context;

		private IUserProtocolHelper helper;

		private User remoteUser;
		private Identity localIdentity;
		private HubConnection hub;
		private TransferDirection direction;

		private TransferInfo transfer;
		private DataHandler dataHandler;
		private object handlerContext;

		private ConnectionMonitor monitor;
		private int lastUpdate;

		private Timer activityTimer;
		private int lastActivity;

		private bool disposed;

		#region Constructors

		public UserConnection(IPEndPoint endPoint, Identity localIdentity,
			ConnectionProtocol protocol, IUserProtocolHelper helper) :
			this(localIdentity, protocol, helper)
		{
			if (endPoint == null)
			{
				throw new ArgumentNullException("endPoint");
			}
			context = endPoint.ToString();
			EndPoint = endPoint;
		}

		public UserConnection(Socket client, Identity localIdentity,
			ConnectionProtocol protocol, IUserProtocolHelper helper) :
			this(localIdentity, protocol, helper)
		{
			if (client == null)
			{
				throw new ArgumentNullException("client");
			}
			Client = client;
		}

		private UserConnection(Identity localIdentity, ConnectionProtocol protocol,
			IUserProtocolHelper helper) : base()
		{
			if (localIdentity == null)
			{
				throw new ArgumentNullException("localIdentity");
			}
			if (helper == null)
			{
				throw new ArgumentNullException("helper");
			}
			this.localIdentity = localIdentity;
			this.helper = helper;

			if (protocol == ConnectionProtocol.Nmdc)
			{
				Protocol = new Nmdc.UserProtocol(this, helper);
			}
			// else: The protocol is identified in ParseData

			transfer = new TransferInfo();
			monitor = new ConnectionMonitor();

			activityTimer = new Timer();
			activityTimer.Elapsed += new ElapsedEventHandler(OnTimerElapsed);
			activityTimer.Interval = 2000;
		}

		#endregion

		#region Properties

		/// <summary>
		/// Gets or sets the remote user.
		/// </summary>
		/// <value>The remote user.</value>
		public User RemoteUser
		{
			get { return remoteUser; }
			set { remoteUser = value; }
		}

		/// <summary>
		/// Gets or sets the local user identity.
		/// </summary>
		/// <value>The local user identity.</value>
		public Identity LocalIdentity
		{
			get { return localIdentity; }
			set { localIdentity = value; } 
		}

		/// <summary>
		/// Gets or sets the hub this connection was established on.
		/// </summary>
		/// <value>The hub this connection was established on.</value>
		public HubConnection Hub
		{
			get { return hub; }
			set { hub = value; }
		}

		#region Transfer

		public TransferDirection Direction
		{
			get { return direction; }
			internal set { direction = value; }
		}

		public TransferInfo TransferInfo
		{
			get { return transfer; }
		}

		public bool Transferring
		{
			get { return transfer.Request != null; }
		}

		public bool Downloading
		{
			get { return direction == TransferDirection.Down && Transferring; }
		}

		public bool Uploading
		{
			get { return direction == TransferDirection.Up && Transferring; }
		}

		public ConnectionMonitor Monitor
		{
			get { return monitor; }
		}

		#endregion

		protected IUserProtocol UserProtocol
		{
			get { return (IUserProtocol)Protocol; }
		}

		#endregion

		#region Methods

		/// <summary>
		/// Checks if the remote client supports a specific request type.
		/// </summary>
		/// <param name="type">The <see cref="RequestType"/> to check.</param>
		/// <returns>True if the request is supported; otherwise, false.</returns>
		public bool Supports(RequestType type)
		{
			return this.UserProtocol.Supports(type);
		}

		#region Connection

		protected override void HandleSocketConnected()
		{
			context = Client.RemoteEndPoint.ToString();
			activityTimer.Start();

			base.HandleSocketConnected();
		}

		protected override void ParseData(byte[] buffer, int offset, int length)
		{
			if (Protocol == null)
			{
				Protocol = new Nmdc.UserProtocol(this, helper);
				Protocol.Initialize();
			}
			base.ParseData(buffer, offset, length);
		}

		protected override void HandleMessage(string message)
		{
			log.Debug("Received: " + message, null, context);

			lastActivity = Environment.TickCount;
			base.HandleMessage(message);
		}

		internal override void Send(string message, System.Text.Encoding encoding)
		{
			lastActivity = Environment.TickCount;
			base.Send(message, encoding);

			log.Debug("Sent: " + message, null, context);
		}

		private void OnTimerElapsed(object sender, ElapsedEventArgs args)
		{
			uint elapsed = (uint)(Environment.TickCount - lastActivity);
			if (elapsed >= 10000)
			{
				OnError(ConnectionError.Timeout, null);
				Disconnect();
			}
		}

		#endregion

		#region Upload

		internal void Upload(Request request, Stream response, long count)
		{
			Upload(request, response, count, false);
		}

		internal void Upload(Request request, Stream response, long count,
			bool compress)
		{
			if (request == null)
			{
				throw new ArgumentNullException("request");
			}
			if (response == null)
			{
				throw new ArgumentNullException("response");
			}
			if (Transferring)
			{
				throw new InvalidOperationException("File transfer in progress.");
			}
			transfer = new TransferInfo(request, count);

			log.Info(String.Format("Uploading {0}", request), null, context);
			OnTransferStarted();

			Stream outputStream = null;
			try
			{
				outputStream = new NetworkStream(Client, false);
				if (compress)
				{
					outputStream = new DeflaterOutputStream(outputStream,
						new Deflater(Deflater.BEST_SPEED));
				}

				// Send
				byte[] bytes = new byte[BufferSize];
				while (transfer.Position < transfer.Length)
				{
					int length = response.Read(bytes, 0, bytes.Length);
					if (length > 0)
					{
						outputStream.Write(bytes, 0, length);
						transfer.Position += length;

						monitor.BytesSent(length);
						UpdateMonitor();
					}
					else
					{
						log.Error("Unexpected end of stream", null, context);
						OnTransferAborted(TransferError.Unknown, "Unexpected end of stream");
						break;
					}
					lastActivity = Environment.TickCount;
				}
				if (transfer.Position == transfer.Length)
				{
					log.Info("Upload finished", null, context);
					OnTransferCompleted();
				}
			}
			catch (Exception)
			{
				OnTransferAborted(TransferError.Aborted, "Socket shut down");
				Disconnect();
			}
			finally
			{
				response.Close();
				if (outputStream != null)
				{
					outputStream.Close();
				}
				ResetTransfer();
			}
		}

		#endregion

		#region Download

		/// <summary>
		/// Attempts to download data from the <see cref="User"/>.
		/// </summary>
		/// <param name="request">
		/// A <see cref="Request"/> that identifies what to download.
		/// </param>
		/// <param name="dataHandler">
		/// A <see cref="DataHandler"/> that will handle the incoming data.
		/// </param>
		/// <param name="context">The object to pass to the handler method.</param>
		public void Download(Request request, DataHandler dataHandler,
			object context)
		{
			if (request == null)
			{
				throw new ArgumentNullException("request");
			}
			if (dataHandler == null)
			{
				throw new ArgumentNullException("dataHandler");
			}
			if (Transferring)
			{
				throw new InvalidOperationException("File transfer in progress.");
			}
			this.transfer = new TransferInfo(request, 0);
			this.dataHandler = dataHandler;
			this.handlerContext = context;

			log.Info(String.Format("Downloading {0}", request), null, this.context);

			// Request the file
			this.UserProtocol.Download(request);
		}

		protected override int HandleData(byte[] buffer, int offset, int count)
		{
			if (Direction != TransferDirection.Down || !Transferring)
			{
				// Download aborted. Discard the data.
				return count;
			}

			if (transfer.Position + count > transfer.Length)
			{
				count = (int)(transfer.Length - transfer.Position);
			}
			long remaining = transfer.Length - transfer.Position - count;

			dataHandler(buffer, offset, count, remaining, handlerContext);
			transfer.Position += count;

			monitor.BytesReceived(count);
			UpdateMonitor();

			lastActivity = Environment.TickCount;

			if (transfer.Position == transfer.Length)
			{
				log.Info("Download complete", null, context);
				ResetTransfer();

				Mode = ConnectionMode.Message;
				OnTransferCompleted();
			}
			return count;
		}

		internal void BeginReceive(long count)
		{
			transfer.Length = count;

			Mode = ConnectionMode.Data;
		}

		internal void DownloadError(TransferError error, string reason)
		{
			log.Error("Download error: " + error + " - " + reason, null, context);

			ResetTransfer();
			OnTransferAborted(error, reason);
		}

		#endregion

		private void ResetTransfer()
		{
			transfer = new TransferInfo();
			dataHandler = null;
			handlerContext = null;
		}

		private void UpdateMonitor()
		{
			uint elapsed = (uint)(Environment.TickCount - lastUpdate);
			if (elapsed >= 1000)
			{
				monitor.TimePeriodPassed();
				lastUpdate = Environment.TickCount;
			}
		}

		protected override void OnError(ConnectionError error, Exception e)
		{
			log.Error(error.ToString(), e, context);
			base.OnError(error, e);
		}

		protected override void OnStateChanged()
		{
			log.Info(State.ToString(), null, context);
			base.OnStateChanged();
		}

		protected virtual void OnUploadRequest(UploadRequestEventArgs args)
		{
			Debug.Assert(args.Request != null);
			if (UploadRequest != null)
			{
				UploadRequest(this, args);
			}
		}

		protected virtual void OnTransferStarted()
		{
			if (TransferStarted != null)
			{
				TransferStarted(this, EventArgs.Empty);
			}
		}

		protected virtual void OnTransferCompleted()
		{
			if (TransferCompleted != null)
			{
				TransferCompleted(this, EventArgs.Empty);
			}
		}

		protected virtual void OnTransferAborted(TransferError error,
			string message)
		{
			if (TransferAborted != null)
			{
				TransferAborted(this, new TransferErrorEventArgs(error, message));
			}
		}

		internal void EmitUploadRequest(UploadRequestEventArgs args)
		{
			OnUploadRequest(args);
		}

		#region IDisposable Members

		protected override void Dispose(bool disposing)
		{
			if (disposed)
			{
				return;
			}

			disposed = true;
			if (disposing)
			{
				if (Transferring)
				{
					log.Info("Transfer aborted", null, context);
					ResetTransfer();

					OnTransferAborted(TransferError.Aborted, "Connection disconnected");
				}
				activityTimer.Dispose();
			}
			base.Dispose(disposing);
		}

		#endregion

		#endregion
	}
}
